/*
 * (c) Copyright IBM Corp. 2000, 2001.
 * All Rights Reserved.
 */
package org.epic.perleditor.templates.textmanipulation;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;


/**
 * A helper class to arrange <code>TextEdit</code>s into a tree to optimize their
 * execution.
 */
/* package */ abstract class TextEditNode {

	/* package */ TextEditNode fParent;
	/* package */ List fChildren;
	/* package */ TextEdit fEdit;
	
	/* package */ static class DefaultNode extends TextEditNode {
		public DefaultNode(TextEdit edit) {
			super(edit);
		}
	}
	
	/* package */ static class RootNode extends TextEditNode {
		private int fUndoIndex;
		public RootNode(int length) {
			super(new NopTextEdit(new TextRange(0, length)));
			fEdit.isSynthetic= true;
		}
		public boolean covers(TextEditNode node) {
			return true;
		}
		public UndoMemento performDo(TextBuffer buffer, IProgressMonitor pm) throws CoreException {
			DoRangeUpdater updater= new DoRangeUpdater();
			UndoMemento undo= new UndoMemento(TextBufferEditor.UNDO);
			try {
				buffer.registerUpdater(updater);
				performDo(buffer, updater, undo, pm);
			} finally {
				buffer.unregisterUpdater(updater);
				updater.setActiveNode(null);
			}
			return undo;
		}
		public UndoMemento performUndo(TextBuffer buffer, IProgressMonitor pm) throws CoreException {
			UndoRangeUpdater updater= new UndoRangeUpdater(this);
			UndoMemento undo= new UndoMemento(TextBufferEditor.REDO);
			try {
				buffer.registerUpdater(updater);
				performUndo(buffer, updater, undo, pm);
			} finally {
				buffer.unregisterUpdater(updater);
				updater.setActiveNode(null);
			}
			return undo;
		}
		
		protected void setUndoIndex(int index) {
			fUndoIndex= index;
		}
		
		protected int getUndoIndex() {
			return fUndoIndex;
		}
	}
	
	/* package */ abstract static class AbstractMoveNode extends TextEditNode {
		private int state;		
		
		private int fTargetIndex;
		private int fSourceIndex;
		
		private List fAffectedChildren;
		
		public AbstractMoveNode(TextEdit edit) {
			super(edit);
			reset();
		}
		protected abstract TextRange getSourceRange();
		protected abstract TextRange getTargetRange();
		protected abstract boolean isUpMove();
		protected boolean isDownMove() {
			return !isUpMove();
		}
		public boolean isMove() {
			return true;
		}
		protected void checkRange(DocumentEvent event) {
			TextRange range= getChildRange();
			int eventOffset= event.getOffset();
			int eventLength= event.getLength();
			int eventEnd = eventOffset + eventLength - 1;
			// "Edit changes text that lies outside its defined range"
	//		Assert.isTrue(range.fOffset <= eventOffset && eventEnd <= range.getInclusiveEnd());
		}
		protected boolean activeNodeChanged(int delta) {
			TextRange targetRange= getTargetRange();
			TextRange sourceRange= getSourceRange();
			switch (state) {
				case 0: // the move delete
					init();
	//				Assert.isTrue(Math.abs(delta) == sourceRange.fLength);
					if (isUpMove()) {
						updateOffset(fAffectedChildren, delta);
						targetRange.fOffset+= delta;
					}
					sourceRange.fLength= 0;
					state= 1;
					break;
				case 1:
					TextEditNode target= (TextEditNode)fParent.fChildren.get(fTargetIndex);
					TextEditNode source= (TextEditNode)fParent.fChildren.get(fSourceIndex);
					updateOffset(source.fChildren, targetRange.fOffset - sourceRange.fOffset);
					target.fChildren= source.fChildren;
					if (target.fChildren != null) {
						for (Iterator iter= target.fChildren.iterator(); iter.hasNext();) {
							((TextEditNode)iter.next()).fParent= target;
						}
					}
					source.fChildren= null;				
					if (isDownMove()) {
						updateOffset(fAffectedChildren, delta);
						sourceRange.fOffset+= delta;
					}
					targetRange.fLength= delta;
					reset();
					break;
			}
			return true;
		}
		private static void updateOffset(List nodes, int delta) {
			if (nodes == null)
				return;
			for (int i= nodes.size() - 1; i >= 0; i--) {
				TextEditNode node= (TextEditNode)nodes.get(i);
				TextRange range= node.getTextRange();
				range.fOffset+= delta;
				updateOffset(node.fChildren, delta);
			}
		}
		private void init() {
			TextRange source= getSourceRange();
			TextRange target= getTargetRange();
			List children= fParent.fChildren;
			for (int i= children.size() - 1; i >= 0; i--) {
				TextEditNode child= (TextEditNode)children.get(i);
				TextRange range= child.fEdit.getTextRange();
				if (range == source)
					fSourceIndex= i;
				else if (range == target)
					fTargetIndex= i;
			}
			int start= Math.min(fTargetIndex, fSourceIndex);
			int end= Math.max(fTargetIndex, fSourceIndex);
			fAffectedChildren= new ArrayList(3);
			for (int i= start + 1; i < end; i++) {
				fAffectedChildren.add(children.get(i));
			}
		}
		private void reset() {
			state= 0;
			fSourceIndex= -1;
			fTargetIndex= -1;
		}
	}
	
	/* package */ static class MoveNode extends AbstractMoveNode {
		public MoveNode(TextEdit edit) {
			super(edit);
		}
		protected TextRange getChildRange() {
			return ((MoveTextEdit)fEdit).getChildRange();
		}
		protected TextRange getSourceRange() {
			return ((MoveTextEdit)fEdit).getSourceRange();
		}
		protected TextRange getTargetRange() {
			return ((MoveTextEdit)fEdit).getTargetRange();
		}
		protected boolean isUpMove() {
			return ((MoveTextEdit)fEdit).isUpMove();
		}
		public boolean isMovePartner(TextEditNode other) {
			if (!(other instanceof TargetMarkNode))
				return false;
			return fEdit == ((MoveTextEdit.TargetMark)other.fEdit).getMoveTextEdit();
		}
		public boolean covers(TextEditNode node) {
			if (node instanceof TargetMarkNode) {
				MoveTextEdit.TargetMark edit= (MoveTextEdit.TargetMark)node.fEdit;
				if (edit.getMoveTextEdit() == fEdit)
					return false;
			}
			return getParentRange().covers(node.getChildRange());
		}
	}
	
	/* package */ static class TargetMarkNode extends AbstractMoveNode {
		public TargetMarkNode(TextEdit edit) {
			super(edit);
		}
		protected TextRange getChildRange() {
			return ((MoveTextEdit.TargetMark)fEdit).getMoveTextEdit().getChildRange();
		}
		protected TextRange getSourceRange() {
			return ((MoveTextEdit.TargetMark)fEdit).getMoveTextEdit().getSourceRange();
		}
		protected TextRange getTargetRange() {
			return ((MoveTextEdit.TargetMark)fEdit).getMoveTextEdit().getTargetRange();
		}
		protected boolean isUpMove() {
			return ((MoveTextEdit.TargetMark)fEdit).getMoveTextEdit().isUpMove();
		}
		public boolean isMovePartner(TextEditNode other) {
			return ((MoveTextEdit.TargetMark)fEdit).getMoveTextEdit() == other.fEdit;
		}
	}

	//---- Range updating ---------------------------------------------------------------------------

	private static abstract class RangeUpdater implements IDocumentListener {
		protected TextEditNode fActiveNode;
		public void documentAboutToBeChanged(DocumentEvent event) {
		}
		public void setActiveNode(TextEditNode node) {
			fActiveNode= node;
		}
		public void updateParents(int delta) {
			TextEditNode node= fActiveNode.fParent;
			while (node != null) {
				node.childNodeChanged(delta);
				node= node.fParent;
			}
		}
		public static int getDelta(DocumentEvent event) {
			return (event.getText() == null ? 0 : event.getText().length()) - event.getLength();
		}
	}
	private static class DoRangeUpdater extends RangeUpdater {
		private List fProcessedNodes= new ArrayList(10);
		public void setActiveNode(TextEditNode node) {
			if (fActiveNode != null)
				fProcessedNodes.add(fActiveNode);
			super.setActiveNode(node);
		}
		public void documentChanged(DocumentEvent event) {
			fActiveNode.checkRange(event);
			int delta= getDelta(event);
			if (!fActiveNode.activeNodeChanged(delta)) {
				for (Iterator iter= fProcessedNodes.iterator(); iter.hasNext();) {
					((TextEditNode)iter.next()).previousNodeChanged(delta);
				}
			}
			updateParents(delta);
		}
	}
	private static class UndoRangeUpdater  extends RangeUpdater {
		private RootNode fRootNode;
		public UndoRangeUpdater(RootNode root) {
			fRootNode= root;
		}
		public void setActiveNode(TextEditNode node) {
			super.setActiveNode(node);
		}
		public void documentChanged(DocumentEvent event) {
			fActiveNode.checkRange(event);
			int delta= getDelta(event);
			if (!fActiveNode.activeNodeChanged(delta)) {
				int start= fRootNode.getUndoIndex() + 1;
				List children= fRootNode.fChildren;
				int size= children != null ? children.size() : 0;
				for (int i= start; i < size; i++) {
					updateUndo((TextEditNode)children.get(i), delta);
				}
			}
			updateParents(delta);
		}
		private void updateUndo(TextEditNode node, int delta) {
			node.previousNodeChanged(delta);
			List children= node.fChildren;
			int size= children != null ? children.size() : 0;
			for (int i= 0; i < size; i++) {
				updateUndo((TextEditNode)children.get(i), delta);
			}
		}
	}
	
	//---- Creating instances ---------------------------------------------------------------------------
	
	static TextEditNode create(TextEdit edit) {
		if (edit instanceof MoveTextEdit)
			return new MoveNode(edit);
		if (edit instanceof MoveTextEdit.TargetMark)
			return new TargetMarkNode(edit);
		return new DefaultNode(edit);
	}
	
	static RootNode createRoot(int length) {
		return new RootNode(length);
	}
	
	private TextEditNode(TextEdit edit) {
		fEdit= edit;
	}
	
	//---- Adding children ---------------------------------------------------------------------------
	
	protected void add(TextEditNode node) {
		if (fChildren == null) {
			fChildren= new ArrayList(1);
			node.fParent= this;
			fChildren.add(node);
			return;
		}
		// Optimize using binary search
		for (Iterator iter= fChildren.iterator(); iter.hasNext();) {
			TextEditNode child= (TextEditNode)iter.next();
			if (child.covers(node)) {
				child.add(node);
				return;
			}
		}
		for (int i= 0; i < fChildren.size(); ) {
			TextEditNode child= (TextEditNode)fChildren.get(i);
			if (node.covers(child)) {
				fChildren.remove(i);
				node.add(child);
			} else {
				i++;
			}
		}
		node.fParent= this;
		fChildren.add(node);
	}
	
	public boolean covers(TextEditNode node) {
		return false; 
	}
	
	//---- Accessing --------------------------------------------------------------------------------------
	
	protected RootNode getRoot() {
		TextEditNode candidate= this;
		while(candidate.fParent != null)
			candidate= candidate.fParent;
		return (RootNode)candidate;
	}
	
	//---- Query interface --------------------------------------------------------------------------------
	
	protected boolean isSynthetic() {
		return fEdit.isSynthetic;
	}
	
	public boolean isMove() {
		return false;
	}
	
	//---- Accessing Ranges ------------------------------------------------------------------------------
	
	protected void checkRange(DocumentEvent event) {
		TextRange range= getTextRange();
		int eventOffset= event.getOffset();
		int eventLength= event.getLength();
		int eventEnd = eventOffset + eventLength - 1;
		// "Edit changes text that lies outside its defined range"
	//	Assert.isTrue(range.fOffset <= eventOffset && eventEnd <= range.getInclusiveEnd());
	}
	
	protected TextRange getTextRange() {
		return fEdit.getTextRange();
	}
	
	protected TextRange getChildRange() {
		return getTextRange();
	}
	
	protected TextRange getParentRange() {
		return getTextRange();
	}
		
	public boolean validate(int bufferLength) {
		if (fChildren == null)
			return true;
		// Only Moves and Nops can be parents	
		if (!(fEdit instanceof MoveTextEdit || fEdit instanceof NopTextEdit))
			return false;
		TextRange lastRange= null;
		for (Iterator iter= fChildren.iterator(); iter.hasNext(); ) {
			TextEditNode node= (TextEditNode)iter.next();
			if (!node.validate(bufferLength))
				return false;
			TextRange range= node.fEdit.getTextRange();
			if (!range.isValid() || range.fOffset + range.fLength > bufferLength)
				return false;
			if (lastRange != null && !(range.isInsertionPointAt(lastRange.fOffset) || range.liesBehind(lastRange)))
				return false;
			lastRange= range;
		}
		return true;
	}

	//---- Updating ----------------------------------------------------------------------------------------
	
	protected boolean activeNodeChanged(int delta) {
		TextRange range= getTextRange();
		range.fLength+= delta;
		// we didn't adjust any processed nodes.
		return false;
	}
	
	protected void previousNodeChanged(int delta) {
		TextRange range= getTextRange();
		range.fOffset+= delta;
	}
	
	protected void childNodeChanged(int delta) {
		getTextRange().fLength+= delta;
	}

	//---- Do it ---------------------------------------------------------------------------------------------
	
	protected void performDo(TextBuffer buffer, RangeUpdater updater, UndoMemento undo, IProgressMonitor pm) throws CoreException {
		int size= fChildren != null ? fChildren.size() : 0;
		for (int i= size - 1; i >= 0; i--) {
			TextEditNode child= (TextEditNode)fChildren.get(i);
			child.performDo(buffer, updater, undo, pm);
		}
		updater.setActiveNode(this);
		if (isSynthetic())
			fEdit.perform(buffer);
		else
			undo.add(fEdit.perform(buffer));
		pm.worked(1);
	}
	
	public void performedDo()  {
		int size= fChildren != null ? fChildren.size() : 0;
		for (int i= size - 1; i >= 0; i--) {
			TextEditNode child= (TextEditNode)fChildren.get(i);
			child.performedDo();
		}
		fEdit.performed();
	}
	
	//---- Undo it -------------------------------------------------------------------------------------------
	
	protected void performUndo(TextBuffer buffer, RangeUpdater updater, UndoMemento undo, IProgressMonitor pm) throws CoreException {
		int size= fChildren != null ? fChildren.size() : 0;
		for (int i= 0; i < size; i++) {
			setUndoIndex(i);
			TextEditNode child= (TextEditNode)fChildren.get(i);
			child.performUndo(buffer, updater, undo, pm);
		}
		updater.setActiveNode(this);
		if (isSynthetic())
			fEdit.perform(buffer);
		else
			undo.add(fEdit.perform(buffer));
		pm.worked(1);
	}
	
	protected void setUndoIndex(int index) {
	}
	
	public void performedUndo()  {
		int size= fChildren != null ? fChildren.size() : 0;
		for (int i= 0; i < size; i++) {
			TextEditNode child= (TextEditNode)fChildren.get(i);
			child.performedUndo();
		}
		fEdit.performed();
	}
	
//	protected void createUndoList(List list) {
//		int size= fChildren != null ? fChildren.size() : 0;
//		for (int i= 0; i < size; i++) {
//			TextEditNode child= (TextEditNode)fChildren.get(i);
//			child.createUndoList(list);
//		}
//		list.add(this);
//	}	
}

